package ru.pb.game.data.scripts;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.apache.log4j.Logger;

import ru.pb.game.avto_save.AutoSaveRunner;
import ru.pb.game.avto_save.IAutoSave;
import ru.pb.game.data.scripts.annotations.OnLoad;
import ru.pb.game.data.scripts.annotations.OnReload;
import ru.pb.game.data.scripts.annotations.OnShutdown;
import ru.pb.global.compiler.MainCompiler;
import ru.pb.global.compiler.MemoryClassLoader;

/**
 * @author Felixx
 */
@SuppressWarnings("unchecked")
public class ScriptLoader implements IAutoSave
{
	private static final Class<? extends Annotation>[] ANNOTATIONS = new Class[] {OnLoad.class, OnReload.class, OnShutdown.class};

	private static final Logger _log = Logger.getLogger(ScriptLoader.class);
	private static ScriptLoader _instance;

	private Map<String, ScriptClass<?>> _classesList = new HashMap<String, ScriptClass<?>>();
	private Map<Class<? extends Annotation>, List<ScriptMethodInvoke>> _invokeList;

	public static ScriptLoader getInstance()
	{
		if(_instance == null)
		{
			_instance = new ScriptLoader();
		}

		return _instance;
	}

	private ScriptLoader()
	{
		load();

		AutoSaveRunner.getInstance().add(this);
	}

	private void load()
	{
		File jarFile = new File("./libs/pb-scripts.jar");

		if(jarFile.exists())
		{
			try
			{
				JarInputStream jarInputStream = new JarInputStream(new FileInputStream(jarFile));
				JarEntry jarEntry;
				while((jarEntry = jarInputStream.getNextJarEntry()) != null)
				{
					String name = jarEntry.getName().replaceAll("/", "\\.");
					if( !name.contains("$") && name.endsWith(".class"))
					{
						try
						{
							Class<?> clazz = Class.forName(name.replace(".class", ""));

							ScriptClass script_clazz = new ScriptClass(clazz);

							_classesList.put(clazz.getName(), script_clazz);
						}
						catch(ClassNotFoundException e)
						{
							_log.info("not found script: " + name);
						}
					}
				}
			}
			catch(IOException e)
			{
				_log.info("IOException: " + e, e);
			}
		}
		else
		{
			List<File> list = new ArrayList<File>();

			parseDirectory(new File("./data/scripts/"), list);

			if(MainCompiler.getInstance().compile(list))
			{
				MemoryClassLoader loader = MainCompiler.getInstance().getClassLoader();
				for(String name : loader)
				{
					if(name.contains("$"))
					{
						continue;
					}

					try
					{
						Class<?> clazz = loader.loadClass(name);
						ScriptClass script_clazz = new ScriptClass(clazz);

						_classesList.put(name, script_clazz);
					}
					catch(ClassNotFoundException e)
					{
						_log.info("not found script: " + name);
					}
				}
			}
			else
			{
				_log.info("Fail to compile scripts.");
			}

			MainCompiler.getInstance().free();
		}

		_log.info("load " + _classesList.size() + " classes.");
	}

	/**
	 * TODO
	 * сделать вызов @OnReload
	 * 
	 * @param packageA
	 */
	public void reloadClass(String packageA)
	{
		String line = "./data/scripts/" + packageA.replace(".", "/") + ".java";
		File f = new File(line);
		if( !f.exists())
		{
			_log.info("class is not exists " + line);
			return;
		}

		List<ScriptClass> newClasses = new ArrayList<ScriptClass>(1);
		MainCompiler.getInstance().compile(f);

		for(String name : MainCompiler.getInstance().getClassLoader())
		{
			if(name.contains("$"))
			{
				continue;
			}

			try
			{
				Class<?> clazz = MainCompiler.getInstance().getClassLoader().loadClass(name);
				ScriptClass<?> script_clazz = new ScriptClass(clazz);

				newClasses.add(script_clazz);
				_classesList.put(name, script_clazz);
			}
			catch(ClassNotFoundException e)
			{
				_log.info("not found script: " + name);
			}
		}

		MainCompiler.getInstance().free();

		List<ScriptMethodInvoke> invokeList = new ArrayList<ScriptMethodInvoke>(1);

		for(ScriptClass<?> clazz : newClasses)
		{
			ScriptInstance<?> instance = clazz.newInstance();
			for(Class<? extends Annotation> annotation : ANNOTATIONS)
			{
				List<ScriptMethodInvoke> set = instance.getMethodsByAnnotation(annotation);
				if( !set.isEmpty())
				{
					if(annotation == OnLoad.class)
					{
						invokeList.addAll(set);
					}

					if( !_invokeList.containsKey(annotation))
					{
						_invokeList.put(annotation, new ArrayList<ScriptMethodInvoke>());
					}

					_invokeList.get(annotation).addAll(set);
				}
			}
		}

		for(ScriptMethodInvoke invoke : invokeList)
		{
			invoke.invoke();
		}

		_log.info("reload done " + line);
	}

	private void initAnnotationCall()
	{
		_invokeList = new HashMap<Class<? extends Annotation>, List<ScriptMethodInvoke>>(0);

		for(ScriptClass<?> clazz : _classesList.values())
		{
			ScriptInstance<?> instance = clazz.newInstance();

			if(instance != null)
			{
				for(Class<? extends Annotation> annotation : ANNOTATIONS)
				{
					List<ScriptMethodInvoke> set = instance.getMethodsByAnnotation(annotation);

					if( !set.isEmpty())
					{
						if( !_invokeList.containsKey(annotation))
						{
							_invokeList.put(annotation, new ArrayList<ScriptMethodInvoke>(0));
						}

						_invokeList.get(annotation).addAll(set);
					}
				}
			}
		}
	}

	public void invoke(Class<? extends Annotation> a)
	{
		if(_invokeList == null)
		{
			initAnnotationCall();
		}

		List<ScriptMethodInvoke> i = _invokeList.get(a);
		if(i == null)
		{
			return;
		}

		for(ScriptMethodInvoke in : i)
		{
			in.invoke();
		}
	}

	private void parseDirectory(File f, List<File> list)
	{
		for(File z : f.listFiles())
		{
			if(z.isDirectory())
			{
				if(z.isHidden() || z.getName().equals(".svn"))
				{
					continue;
				}

				parseDirectory(z, list);
			}
			else
			{
				if(z.isHidden() || !z.getName().contains(".java"))
				{
					continue;
				}
				list.add(z);
			}
		}
	}

	public void reload()
	{
		invoke(OnReload.class);

		_invokeList.clear();

		load();

		invoke(OnLoad.class); // упс забыл поставить
	}

	public Class<?> getClass(String t) throws ClassNotFoundException
	{
		ScriptClass<?> clazz = _classesList.get(t);
		if(clazz == null)
		{
			throw new ClassNotFoundException();
		}
		return clazz.getSourceClass();
	}

	public Collection<ScriptClass<?>> getAllClasses()
	{
		return _classesList.values();
	}

	@Override
	public void save()
	{}

	@Override
	public void shutdown()
	{
		invoke(OnShutdown.class);
	}
}